(function($) {

	if (typeof($) === 'undefined') {
		throw 'Requires jQuery';
	}
	
	if (typeof(jqkit) === 'undefined' || typeof(jqkit.math) === 'undefined' || typeof(jqkit.userAgent) === 'undefined' || typeof(jqkit.plugins) === 'undefined') {
		throw 'Requires jqkit, jqkit.math, jqkit.userAgent, and jqkit.plugins';
	}
	
	jqkit.plugins.VScroller = function(element, settings) {
		var self = this;
		
		if (jqkit.isDefinedAndNotNull(element) === false) {
			throw 'Html element parameter is not defined';
		}
	};
	
	jqkit.plugins.VScroller = function(element, settings) {
		var self = this;
		
		if (jqkit.isDefinedAndNotNull(element) === false) {
			throw 'Html element parameter is not defined';
		}
		
		if (element instanceof $ === false) {
			element = $(element);
		}
		
		self.viewport_ = element;
        self.content_ = null;

        // Look for existing viewport markup.
        if (settings.contentWrapperSelector !== null) {
            if (typeof(settings.contentWrapperSelector) === 'string') {
                self.content_ = self.viewport_.find(settings.contentWrapperSelector);
            } else if (typeof(settings.contentWrapperSelector) === 'function') {
                self.content_ = settings.contentWrapperSelector(self.viewport_);
            }
            // Set back to null if nothing was found.
            if (self.content_ instanceof $ === false || self.content_.length === 0) {
                self.content_ = null;
            }
        }

        // If viewport is not yet defined, insert it. Otherwise, add plugin specific class name(s).
        if (self.content_ === null) {
            self.viewport_.append('<div class="jqkit-vScroller-viewport" style="height:300px;"></div>');
            self.content_ = self.viewport_.children('div.jqkit-vScroller-viewport');
        } else {
            self.content_.addClass('jqkit-vScroller-viewport');
        }

        var isVisible = (self.viewport_.is(':visible') === true);
        var rows = self.content_.children('[data-row-index]');
        
        if (settings.viewportHeight !== null) {
            self.viewportHeight_ = settings.viewportHeight;
        } else if (isVisible === true && self.viewport_.css('max-height') !== 'none') {
            self.viewportHeight_ = parseInt(self.viewport_.css('max-height'), 10);
        } else {
            self.viewportHeight_ = 300;
        }
        
		self.cacheSize_ = settings.cacheSize || 0;
		self.startIndex_ = settings.startIndex || 0;
        self.rowWidth_ = settings.rowWidth || (isVisible === true ? rows.first().outerWidth(true) : null) || null;
		self.rowHeight_ = settings.rowHeight || (isVisible === true ? rows.first().outerHeight(true) : null) || 30;
		self.totalCount_ = settings.totalCount || 0;
		self.cachedRange_ =  settings.cachedRange || null;
		self.getCachedRows_ = settings.getCachedRows || null;
		self.updateCachedRow_ = settings.updateCachedRow || null;
		self.updateCache_ = settings.updateCache || null;
        self.abortCacheUpdate_ = settings.abortCacheUpdate || null;
		self.onEmptyCache_ = settings.onEmptyCache || null;
        self.getRowClassAttribute_ = settings.getRowClassAttribute || null;
		self.cacheUpdateDelay_ = settings.cacheUpdateDelay || 1000;
		self.scrollJumpDelay_ = settings.scrollJumpDelay || 200;
		self.activityDelay_ = settings.activityDelay || 1500;
        self.largeBufferSize_ = settings.largeBufferSize || Math.round(self.viewportHeight_ / self.rowHeight_);
        self.smallBufferSize_ = settings.smallBufferSize || 3;
        
        self.viewport_.css({
            position: 'relative',
            overflowY: 'scroll',
            height: self.viewportHeight_ + 'px',
            minHeight: 'none',
            maxHeight: 'none'
        });
		
		self.isEnabled_ = true;
		self.scrollDirection_ = 1;
		self.cacheUpdateInProgress_ = false;
		self.activityTimeout_ = null;
		self.isInitialized_ = false;
		self.renderedRange_ = null;

        // Look for current rendered range (if exists).
        if (rows.length > 0) {
            var min = rows.length;
            var max = 0;

            rows.each(function() {
                var rowIndex = this.getAttribute('data-row-index');
                if (jqkit.isDefinedAndNotNull(rowIndex) === true) {
                    rowIndex = parseInt(rowIndex, 10);
                    if (rowIndex < min) {
                        min = rowIndex;
                    }
                    if (rowIndex > max) {
                        max = rowIndex;
                    }
                }
            });

            self.renderedRange_ = new jqkit.math.Range(min, max);
        }

        // Adjust start index to account for buffer size to avoid an immediate cache update when start index is
        // greater than zero.
        if (self.startIndex_ > 0) {
            self.startIndex_ += self.largeBufferSize_;
        }

        // Check if cache is empty.
        if (self.isCacheEmpty_() === true) {
            if (self.onEmptyCache_ !== null) {
                self.onEmptyCache_.call(self.viewport_);
            }
        } else {
		    self.setScrollableHeight_();
		    self.setScrollTop(self.startIndex_ * self.rowHeight_);
        }

		self.viewport_.bind('scroll.jqkit_vScroller', self, function(event) {
			event.data.onScrollHandler_(this.scrollTop, this.scrollLeft);
		});
		
		self.isInitialized_ = true;
	};
	
	jqkit.plugins.VScroller.prototype.viewport_;
	
	jqkit.plugins.VScroller.prototype.content_;
	
	jqkit.plugins.VScroller.prototype.isInitialized_;
	
	jqkit.plugins.VScroller.prototype.isEnabled_;
	
	jqkit.plugins.VScroller.prototype.rowHeight_;
	
	jqkit.plugins.VScroller.prototype.rowWidth_;
	
	jqkit.plugins.VScroller.prototype.viewportHeight_;
	
	jqkit.plugins.VScroller.prototype.scrollHeight_;
	
	jqkit.plugins.VScroller.prototype.cacheSize_;
	
	jqkit.plugins.VScroller.prototype.startIndex_;
	
	jqkit.plugins.VScroller.prototype.totalCount_;
	
	jqkit.plugins.VScroller.prototype.scrollTop_;
	
	jqkit.plugins.VScroller.prototype.scrollDirection_;
	
	jqkit.plugins.VScroller.prototype.scrollingTimeout_;
	
	jqkit.plugins.VScroller.prototype.activityTimeout_;
	
	jqkit.plugins.VScroller.prototype.cachedRange_;
	
	jqkit.plugins.VScroller.prototype.renderedRange_;
	
	jqkit.plugins.VScroller.prototype.getCachedRows_;
	
	jqkit.plugins.VScroller.prototype.updateCachedRows_;
	
	jqkit.plugins.VScroller.prototype.updateCache_;

    jqkit.plugins.VScroller.prototype.abortCacheUpdate_;

    jqkit.plugins.VScroller.prototype.onEmptyCache_;

    jqkit.plugins.VScroller.prototype.getRowClassAttribute_;
	
	jqkit.plugins.VScroller.prototype.cacheUpdateInProgress_;
	
	jqkit.plugins.VScroller.prototype.cacheUpdateDelay_;
	
	jqkit.plugins.VScroller.prototype.scrollJumpDelay_;
	
	jqkit.plugins.VScroller.prototype.largeBufferSize_;
	
	jqkit.plugins.VScroller.prototype.smallBufferSize_;

    jqkit.plugins.VScroller.prototype.onScrollHandler_ = function(scrollTop, scrollLeft) {
		var self = this;
		if (scrollTop !== self.scrollTop_) {
			self.setScrollTop(scrollTop);
		}
	};
	
	jqkit.plugins.VScroller.prototype.setScrollableHeight_ = function() {
		var self = this;
		var totalHeight = ((self.rowHeight_ * self.totalCount_) + jqkit.userAgent.getScrollBarWidth());
		self.content_.css({
			height: totalHeight + 'px'
		});
		self.scrollHeight_ = totalHeight - self.viewportHeight_;
	};
	
	jqkit.plugins.VScroller.prototype.isCacheEmpty_ = function() {
        var self = this;
        return ((!self.renderedRange_ || self.renderedRange_.size() === 0) && (!self.cachedRange_ || self.cachedRange_.size() === 0) && !self.totalCount_);
    };
	
	jqkit.plugins.VScroller.prototype.getVisibleRange_ = function(opt_scrollTop) {
		var self = this;
		var scrollTop = opt_scrollTop || self.scrollTop_;
		var start = Math.floor(scrollTop / self.rowHeight_);
		var end = Math.ceil((scrollTop + self.viewportHeight_) / self.rowHeight_);
		return new jqkit.math.Range(Math.max(start, 0), Math.min(end, (self.totalCount_ - 1)));
	};
	
	jqkit.plugins.VScroller.prototype.getRenderedRange_ = function(opt_scrollTop) {
		var self = this;
		var range = self.getVisibleRange_();
		
		if (self.scrollDirection_ === -1) {
			range.start -= self.largeBufferSize_;
			range.end += self.smallBufferSize_;
		} else if (self.scrollDirection_ === 1) {
			range.start -= self.smallBufferSize_;
			range.end += self.largeBufferSize_;
		} else {
			range.start -= self.smallBufferSize_;
			range.end += self.smallBufferSize_;
		}
		
		range.start = Math.max(range.start, 0);
		range.end = Math.min(range.end, (self.totalCount_ - 1));
		
		return range;
	};
	
	jqkit.plugins.VScroller.prototype.setScrollTop = function(scrollTop) {
		var self = this;
		
		// Constrain scrollTop to bounds of viewport.
		scrollTop = Math.min(Math.max(scrollTop, 0), self.scrollHeight_);
		
		if (self.scrollTop_ === scrollTop && self.renderedRange_ !== null) {
			return;
		}
		
		if (self.isEnabled_ === false) {
			return;
		}

		if (self.activityTimeout_ !== null) {
			clearTimeout(self.activityTimeout_);
			self.activityTimeout_ = null;
		} else {
			self.viewport_.trigger('vScrollStart');
		}
		
		var prevScrollTop = self.scrollTop_;
		var scrollDistance = Math.abs(scrollTop - prevScrollTop);
		var timeoutWasSet = (self.scrollingTimeout_ !== null);
		
		if (timeoutWasSet === true) {
			clearTimeout(self.scrollingTimeout_);
			self.scrollingTimeout_ = null;
		}
        
        // Optional abort cache update request(s) trigger (do NOT do this on initial render).
        if (self.cacheUpdateInProgress_ === true && self.abortCacheUpdate_ !== null) {
            self.abortCacheUpdate_();
        }
		
		// Scroll element to desired position.
		self.viewport_.scrollTop(scrollTop);
		self.scrollTop_ = scrollTop;
		self.scrollDirection_ = prevScrollTop < scrollTop ? 1 : -1;
		
		var visibleRange = self.getVisibleRange_();
		var targetRange = self.getRenderedRange_();

		if (self.renderedRange_ !== null && self.renderedRange_.contains(targetRange) === true) {
			// Do nothing.
            self.cacheUpdateInProgress_ = false;
		} else if (self.cachedRange_ === null || self.cachedRange_.contains(targetRange) === false) {
			self.markAsLoading_(true);
			// Initiate cache update timeout.
			self.scrollingTimeout_ = setTimeout(function() {
				self.scrollingTimeout_ = null;
				self.renderViewport_();
			}, self.cacheUpdateDelay_);
		} else if (timeoutWasSet === false && scrollDistance <= self.viewportHeight_) {
			// If near scroll and if total number of rendered rows is acceptable, simply append new
			// rows (no timeout or updating required).
			self.cacheUpdateInProgress_ === false;
			self.renderViewport_();
		} else {
			// Scroll jump.
			self.cacheUpdateInProgress_ === false;
			self.scrollingTimeout_ = setTimeout(function() {
				self.scrollingTimeout_ = null;
				self.renderViewport_();
			}, self.scrollJumpDelay_);
		}
		
        self.viewport_.trigger('vScroll', [visibleRange.start, visibleRange.end, (self.totalCount_ - 1)]);
		
		if (self.isInitialized_ === true) {
			if (self.scrollTop_ === 0) {
				self.viewport_.trigger('vScrollTop');
			} else if (self.scrollTop_ === self.scrollHeight_) {
				self.viewport_.trigger('vScrollBottom');
			}
		}
		
		(function retry() {
			self.activityTimeout_ = setTimeout(function() {
				if (self.cacheUpdateInProgress_ === false) {
					self.activityTimeout_ = null;
					self.viewport_.trigger('vScrollStop');
				} else {
					retry();
				}
			}, self.activityDelay_);
		})();
	};
	
	jqkit.plugins.VScroller.prototype.renderViewport_ = function() {
		var self = this;
		var targetRange = self.getRenderedRange_();
		var threshold = (targetRange.end - targetRange.start) * 2 + 1;
		var boundingRange = targetRange.clone();
		if (self.renderedRange_ !== null) {
			boundingRange = jqkit.math.Range.boundingRange(self.renderedRange_, targetRange);
		}
		if (self.cachedRange_ === null || self.cachedRange_.contains(targetRange) === false) {
			// Update cache.
			self.markAsLoading_(true);
			self.cacheUpdateInProgress_ = true;
			self.updateCache_(self.getStartIndexForRange_(targetRange), function(newRange) {
				if (self.cacheUpdateInProgress_ === true) {
					// Must set active cache to false before calling updateCacheRange_ which 
					// in turn calls renderViewport_ (which reads this property).
					self.cacheUpdateInProgress_ = false;
					self.updateCacheRange_(newRange);
				}
			});
		} else if ((boundingRange.end - boundingRange.start) > threshold) {
			// If total number of rows hits a certian threshold, remove rows not in targeted range.
			self.removeRows_(targetRange);
			self.appendRows_(targetRange);
			self.markAsLoading_(false);
            self.cacheUpdateInProgress_ = false;
		} else {
			self.appendRows_(boundingRange);
			self.markAsLoading_(false);
            self.cacheUpdateInProgress_ = false;
		}
	};
	
	jqkit.plugins.VScroller.prototype.getStartIndexForRange_ = function(range) {
		var self = this;
		var index = 0;
		var rangeSize = range.end - range.start;
		var offset = Math.max(rangeSize - self.cacheSize_, 3);
		if (self.scrollDirection_ === -1) {
			index = range.start + offset - self.cacheSize_ + rangeSize;
		} else {
			index = range.start - offset;
		}
		return Math.max(index, 0);
	};
	
	jqkit.plugins.VScroller.prototype.updateCacheRange_ = function(newRange) {
		var self = this;
		self.cachedRange_ = newRange;
		// Remove any rendered rows no longer in cache.
		self.removeRows_(self.cachedRange_);
        // Update total row height.
        self.totalCount_ = Math.max(self.cachedRange_.end, self.totalCount_);
		self.setScrollableHeight_();
		self.renderViewport_();
	};
	
	jqkit.plugins.VScroller.prototype.removeRows_ = function(opt_rangeToKeep) {
		var self = this;
		var rangeToKeep = opt_rangeToKeep || new jqkit.math.Range(-1, -1);
		var rangeRemoved = new jqkit.math.Range(-1, -1);
		var markup = null;
		var rowIndex = -1;
		
		// Retrieve and remove markup. Do not bother worrying about if rows are still in cache. Let the
		// data store decide what to do with rows not still in cache.
		self.content_.children().each(function(i) {
			rowIndex = parseInt(this.getAttribute('data-row-index'), 10);
			
			if (rangeToKeep.containsPoint(rowIndex) === false) {
				markup = $(this).remove().html();
				
				if (self.updateCachedRow_ !== null) {
					self.updateCachedRow_(rowIndex, markup);
				}
				
				markup = null;
				
				if (rangeRemoved.start === -1 || rangeRemoved.start > rowIndex) {
					rangeRemoved.start = rowIndex;
				}
				
				if (rangeRemoved.end === -1 || rangeRemoved.end < rowIndex) {
					rangeRemoved.end = rowIndex;
				}
			}
		});
		
		// Remove indexes from rendered range. This will set the range to null if empty.
		if (self.renderedRange_ !== null) {
			self.renderedRange_ = jqkit.math.Range.subtract(self.renderedRange_, rangeRemoved);
		}
	};
	
	jqkit.plugins.VScroller.prototype.appendRows_ = function(range) {
		var self = this;
		var rangeToAppend = range.clone();

		if (self.renderedRange_ !== null) {
			// Do not append rows that are already rendered.
			rangeToAppend = jqkit.math.Range.subtract(rangeToAppend, self.renderedRange_);
		}
		
		if (rangeToAppend !== null) {
			var rowsToAppend = self.getCachedRows_(rangeToAppend.start, rangeToAppend.end);
			var markup = self.buildMarkup_(rowsToAppend, rangeToAppend);
			
			rowsToAppend = null;
			
			self.content_.append(markup);
			
			markup = null;
			
			self.renderedRange_ = range;
		}
	};
	
	jqkit.plugins.VScroller.prototype.replaceRows_ = function(range) {
		var self = this;
		
		var rowsToAppend = self.getCachedRows_(range.start, range.end);
		var markup = self.buildMarkup_(rowsToAppend, range);
		
		rowsToAppend = null;
		
		self.content_.html(markup);
		
		markup = null;
		
		self.renderedRange_ = range;
	};
	
	jqkit.plugins.VScroller.prototype.buildMarkup_ = function(rows, range) {
		var self = this;
		var markup = '';
        var rowWidthStr = (self.rowWidth_ !== null ? (self.rowWidth_ + 'px') : 'auto');
		var rowStyle = 'overflow:hidden;position:absolute;left:0px;width:' + rowWidthStr + ';';
		for (var i = 0, length = rows.length; i < rows.length; i++) {
			var style = 'top:' + ((range.start + i) * self.rowHeight_) + 'px;height:' + self.rowHeight_ + 'px;';
            var classAttr = self.getRowClassAttribute_ !== null ? self.getRowClassAttribute_(range.start + i) : '';
			markup += '<div data-row-index="' + (range.start + i) + '"' + ' style="' + (rowStyle + style) + '" class="' + classAttr + '">' + rows[i] + '</div>';
		}
		return markup;
	};
	
	jqkit.plugins.VScroller.prototype.markAsLoading_ = function(value) {
		var self = this;
		if (value === true) {
			self.viewport_.addClass('jqkit-loading');
		} else {
			self.viewport_.removeClass('jqkit-loading');
		}
	};
	
	jqkit.plugins.VScroller.prototype.enable = function() {
		var self = this;
		
		// Reset everything to handle any changes made while component was disabled.
		self.scrollDirection_ = 1;
		self.cacheUpdateInProgress_ = false;
		self.setScrollableHeight_();
		self.viewport_.css({ height: self.viewportHeight_ + 'px' }).removeClass('jqkit-disabled');
		self.content_.show();
		self.isEnabled_ = true;
		self.setScrollTop(self.startIndex_ * self.rowHeight_);
	};
	
	jqkit.plugins.VScroller.prototype.disable = function() {
		var self = this;
		
		self.isEnabled_ = false;
		
		if (self.scrollingTimeout_ !== null) {
			clearTimeout(self.scrollingTimeout_);
			self.scrollingTimeout_ = null;
		}
		
		if (self.activityTimeout_ !== null) {
			clearTimeout(self.activityTimeout_);
			self.activityTimeout_ = null;
		}
		
		self.cacheUpdateInProgress_ === false;
		self.content_.hide();
		self.removeRows_();
		self.viewport_.addClass('jqkit-disabled');
	};
	
	jqkit.plugins.VScroller.prototype.set = function(obj) {
		var self = this;
		self.disable();
		if (typeof(obj.startIndex) !== 'undefined') {
			self.startIndex_ = Math.max(parseInt(obj.startIndex, 10), 0);
		}
		if (typeof(obj.cacheSize) !== 'undefined') {
			self.cacheSize_ = Math.max(parseInt(obj.cacheSize, 10), 0);
		}
		if (typeof(obj.totalCount) !== 'undefined') {
			self.totalCount_ = Math.max(parseInt(obj.totalCount, 10), 0);
		}
		if (typeof(obj.rowHeight) !== 'undefined') {
			self.rowHeight_ = Math.max(parseInt(obj.rowHeight, 10), 0);
		}
		if (typeof(obj.rowWidth) !== 'undefined') {
			self.rowWidth_ = Math.max(parseInt(obj.rowWidth, 10), 0);
		}
		if (typeof(obj.viewportHeight) !== 'undefined') {
			self.viewportHeight_ = Math.max(parseInt(obj.viewportHeight, 10), 0);
		}
		if (typeof(obj.cachedRange) !== 'undefined') {
			self.cachedRange_ = obj.cachedRange;
		}
		if (typeof(obj.getCachedRows) !== 'undefined') {
			self.getCachedRows_ = obj.getCachedRows;
		}
		if (typeof(obj.updateCachedRow) !== 'undefined') {
			self.updateCachedRow_ = obj.updateCachedRow;
		}
		if (typeof(obj.updateCache) !== 'undefined') {
			self.updateCache_ = obj.updateCache;
		}
		self.enable();
	};
	
	jqkit.plugins.VScroller.prototype.scrollTo = function(index) {
		var self = this;
		self.setScrollTop(self.rowHeight_ * Math.max(parseInt(index, 10), 0));
	};
	
	jqkit.plugins.VScroller.prototype.scroll = function(increment) {
		var self = this;
		self.setScrollTop(self.scrollTop_ + parseInt(increment, 10));
	};
	
	$.fn.vScroller = function(options) {
		if (typeof(options) === 'string') {
			var scroller = $(this).data('jqkit.plugins.vScroller');
			
			if (jqkit.isDefinedAndNotNull(scroller) === false) {
				throw 'vScroller not initialized';
			}
			
			if (options === 'setScrollTop') {
				scroller.setScrollTop(arguments[1] || 0);
			} else if (options === 'scrollTo') {
				scroller.scrollTo(arguments[1] || 0);
			} else if (options === 'scroll') {
				scroller.scroll(arguments[1] || 0);
			} else if (options === 'enable') {
				scroller.enable();
			} else if (options === 'disable') {
				scroller.disable();
			} else if (options === 'set') {
				scroller.set(arguments[1] || {});
			} else {
				throw 'method not found';
			}
			
			return this;
		}
		
		return $(this).each(function() {
			var viewport = $(this);
			viewport.data('jqkit.plugins.vScroller', new jqkit.plugins.VScroller(viewport, $.extend({}, $.fn.vScroller.defaults, options)));
		});
	};
	
	$.fn.vScroller.defaults = {
		startIndex: 0,
		cacheSize: 100,
		totalCount: null,
		rowHeight: null,
		viewportHeight: null,
        contentWrapperSelector: null,
        cachedRange: null,
        rowWidth: null,
		getCachedRows: function(start, end) {
			throw 'getCachedRows not implemented';
		},
		updateCachedRow: function(index, markup) {
			throw 'updateCachedRow not implemented';
		},
		updateCache: function(start, end) {
			throw 'updateCache not implemented';
		},
        onEmptyCache: null,
        abortCacheUpdate: null,
        getRowClassAttribute: null,
        largeBufferSize: null,
        smallBufferSize: null
	};

})(jQuery);